/**
 * @(#)RetentTaskTimer.java 2016年11月11日
 * Copyright(C) 2016 ZHEXIN IT CO.,LTD. All rights reserved.
 */
package org.jsbd.boss.timer;

import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.mina.util.ConcurrentHashSet;
import org.jsbd.boss.common.redis.RedisClient;
import org.jsbd.boss.dao.IRetentTimesDao;
import org.jsbd.boss.domian.ChannelApkConfigTimeModel;
import org.jsbd.boss.domian.ChannelRetentLogInfo;
import org.jsbd.boss.domian.channel.*;
import org.jsbd.boss.service.*;
import org.jsbd.boss.service.impl.CacheParamManager;
import org.jsbd.boss.util.CacheKey;
import org.jsbd.boss.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 留存任务的timer
 *
 * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
 * @version 1.0.0
 */
@Service
public class RetentTaskTimer extends BaseTaskTimer {
    public static final Logger LOGGER = LoggerFactory.getLogger(RetentTaskTimer.class);
    @Resource
    private IChannelRetentService channelRetentService;
    @Resource
    private RedisClient redisClient;
    @Resource
    private IChannelApkInfoService channelApkInfoService;

    @Resource
    private IChannelLimitService channelLimitService;
    @Resource
    private ApkManager apkManager;

    @Resource
    private CacheParamManager cacheParamManager;

    @Resource
    private IRetentFillConfigTimerService configTimerService;

    @Resource
    private IRetentTimesDao retentTimesDao;

    private Map<String, Integer> markApkIdMap = new HashMap<>();
    private Map<Integer, String> apkIdMarkMap = new HashMap<>();

    private Map<String, List<RetentTimesConfig>> markRetentTimesMap;


    //记录开始执行的时候的时间天,避免定时器时间太长
    private long dayOfTime;

    @Override
    protected boolean isCanDo() {
        Object retentTaskFlag = cacheParamManager.getParamValue(CacheKey.RETENT_TASK_FLAG);
        if (!(retentTaskFlag != null && "0".equals(retentTaskFlag))) {
            return true;
        } else {
            redisClient.remove(CacheKey.VERR_APK_KEY3);
            return false;
        }
    }

    @Override
    public void execute() {
        long expireTime = 30;
        Object refreshTime = cacheParamManager.getParamValue(CacheKey.RETENT_TASK_REFRESH);
        if (refreshTime != null) {
            expireTime = Long.parseLong(refreshTime.toString());
        }
        if (redisClient.evalSetNx(CacheKey.RETENT_TIMER_LOCK_KEY, expireTime)) {
            long now = System.currentTimeMillis();
            initApkMap();
            initRetentTimesMap();

            dayOfTime = DateUtil.getDaysOfTime();

            // mark 天对应的任务量 mark : day : num
            Map<String, Map<String, Integer>> markDayNumMap = new ConcurrentHashMap<>(4);
            // mark总量
            Map<String, Integer> markNumMap = new ConcurrentHashMap<>(4);
            // mark对应的天数以及配置
            Map<String, Set<MarkDayTask>> markDaySet = new ConcurrentHashMap<>(4);
            // 需要计算的天数
            Set<String> daySet = new ConcurrentHashSet<>();

            //拉活记录
            Map<String, List<MarkDayTask>> markBrushMap = new ConcurrentHashMap<>(4);

            // 1、查询所有apk留存配置 2、各个mark总量 已经使用量
            List<ApkRetentConfig> allRetentApks = channelApkInfoService.selectAllSimpleRetentApkInfos();

            //查询留存次数配置
            initTaskNumInfo(markDaySet, daySet, allRetentApks);
            // 更新天对应的留存任务
            updateDayRetentTask(daySet);
            // 更新任务缓存
            updateCache(markDaySet, markDayNumMap, markNumMap, markBrushMap, daySet);
            // 更新没用的包任务
            updateUnUseApk(markDayNumMap);
            // 需要执行的留存apk
            updateCanUseApks(markNumMap);
            markDayNumMap.clear();
            markNumMap.clear();
            markDaySet.clear();
            daySet.clear();
            LOGGER.warn("留存任务定时器更新花费时间{}秒", ((System.currentTimeMillis() - now) / 1000));
        }
    }


    private void initTaskNumInfo(Map<String, Set<MarkDayTask>> markDaySet, Set<String> daySet, List<ApkRetentConfig> allRetentApks) {
        if (allRetentApks != null && allRetentApks.size() > 0) {
            for (ApkRetentConfig channelApk : allRetentApks) {
                Integer apkId = channelApk.getId();
                String mark = apkIdMarkMap.get(apkId);
                // 执行前面的某天的数据
                Integer day = channelApk.getRetentDay();
                String someDay = DateUtil.getPreSomeDay(day);
                MarkDayTask markDayTask = new MarkDayTask();
                markDayTask.setApkId(apkId);
                markDayTask.setMark(mark);
                markDayTask.setExecTimes(channelApk.getExetimes());
                markDayTask.setDay(someDay);
                Integer isRetentLimit = channelApk.getIsRetentLimit();
                if (isRetentLimit != null && isRetentLimit == 1) {
                    markDayTask.setLimit(true);
                } else {
                    markDayTask.setLimit(false);
                }
                markDayTask.setRate(channelApk.getRetentRate());
                markDayTask.setBrushRate(channelApk.getBrushRate());
                Set<MarkDayTask> markSet = markDaySet.get(mark);
                if (markSet == null) {
                    markSet = new HashSet<>();
                }
                markDayTask.setPriorty(channelApk.getPriority());
                markSet.add(markDayTask);
                daySet.add(someDay);
                markDaySet.put(mark, markSet);
            }
        }
    }

    /**
     * updateDayRetentTask:更新天留存任务信息 当天执行一次
     *
     * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
     */
    private void updateDayRetentTask(Set<String> daySet) {

        if (daySet != null && daySet.size() > 0) {
            if (redisClient.evalSetNx(CacheKey.R_TIMES_LOCK, DateUtil.leftTimeToMidNight() / 1000)) {
                // 更新天对应的留存任务
                String[] days = daySet.toArray(new String[0]);
                for (String day : days) {
                    try {
                        // 留存任务信息 先更新次数
                        channelRetentService.updateExecTimesOfDay(day);
                        LOGGER.info("更新天{}需要执行留存任务的次数成功", day);
                    } catch (Exception e) {
                        LOGGER.error("更新天{}需要执行留存任务的次数出错", day, e);
                    }
                }
                redisClient.remove(CacheKey.R_I_D_R);
                // 重置补量配置
                configTimerService.updateConfigOfTimer();
            }
        }
    }


    /**
     * getMarkOfDay:获取mark对应天数下的量
     *
     * @param dayMarksMap
     * @return
     * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
     */
    private void initMarkOfDay(MarkDayTask markDay,
                               Map<String, Set<MarkDayTask>> dayMarksMap,
                               Map<String, Map<String, Integer>> markDayNumMap,
                               Map<String, List<MarkDayTask>> markBrushMap,
                               Map<String, Integer> markTotalMap) {
        // set<mark>
        Set<MarkDayTask> dayMarkCounts = dayMarksMap.get(markDay.getDay());
        String mark = markDay.getMark();
        Map<String, Integer> markDayMap = markDayNumMap.get(mark);
        if (markDayMap == null) {
            markDayMap = new HashMap<>();
            markDayNumMap.put(mark, markDayMap);
        }

        String key = mark + markDay.getDay();
        if (!markTotalMap.containsKey(key)) {
            markTotalMap.put(key, 0);
        }
        if (dayMarkCounts != null && !dayMarkCounts.isEmpty()) {
            MarkDayTask[] marks = dayMarkCounts.toArray(new MarkDayTask[0]);
            for (MarkDayTask mark1 : marks) {
                if (mark.equals(mark1.getMark())) {
                    int taskNum;
                    int brushTaskNum;
                    Double brushRate = markDay.getBrushRate();
                    if (brushRate == null) {
                        brushRate = 0d;
                    }
                    if (markDay.isLimit()) {
                        double percent = getTimeLimit(mark1.getMark());
                        taskNum = (int) (percent * mark1.getTaskNum() * markDay.getRate() / 10000);
                        brushTaskNum = (int) (percent * mark1.getTaskNum() * brushRate / 10000);
                    } else {
                        taskNum = (int) (mark1.getTaskNum() * markDay.getRate() / 100);
                        brushTaskNum = (int) (mark1.getTaskNum() * brushRate / 100);
                    }
                    Integer dayNum = markDayMap.get(markDay.getDay());
                    if (dayNum != null) {
                        markDayMap.put(markDay.getDay(), taskNum + dayNum);
                    } else {
                        markDayMap.put(markDay.getDay(), taskNum);
                    }
                    int retentTotal = (int) (mark1.getTaskNum() * markDay.getRate() / 100);
                    List<MarkDayTask> markDayTasks = markBrushMap.get(mark);
                    if (markDayTasks == null) {
                        markDayTasks = new ArrayList<>();
                    }
                    MarkDayTask brushRecord = new MarkDayTask();
                    brushRecord.setMark(mark);
                    brushRecord.setTaskNum(brushTaskNum);
                    brushRecord.setDay(mark1.getDay());
                    markDayTasks.add(brushRecord);

                    markBrushMap.put(mark, markDayTasks);
                    markDayNumMap.put(mark, markDayMap);
                    markTotalMap.put(key, markTotalMap.get(key) + retentTotal);
                }

            }
        }
    }

    /**
     * updateCache:更新留存任务信息缓存
     *
     * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
     */
    private void updateCache(Map<String, Set<MarkDayTask>> markDaySet,
                             Map<String, Map<String, Integer>> markDayNumMap,
                             Map<String, Integer> markNumMap,
                             Map<String, List<MarkDayTask>> markBrushMap,
                             Set<String> daySet) {
        // 更新天对应的留存任务
        String expireTime = String.valueOf(DateUtil.leftTimeToMidNight() / 1000);
        Map<String, Set<MarkDayTask>> dayMarksMap = queryMarkNumOfDay(daySet);

        Map<String, Integer> markTotalMap = new HashMap<>();
        // mark:set<day>
        for (String mark : markDaySet.keySet()) {
            // days
            MarkDayTask[] markDayTasks = markDaySet.get(mark).toArray(new MarkDayTask[0]);
            for (MarkDayTask markDay : markDayTasks) {
                initMarkOfDay(markDay, dayMarksMap, markDayNumMap, markBrushMap, markTotalMap);
            }
        }

        String[] markDays1 = markDayNumMap.keySet().toArray(new String[0]);
        if (markDays1 != null && markDays1.length > 0) {
            for (int i = 0; i < markDays1.length; i++) {
                List<String> markDaykeyAndArgs = new ArrayList<>();
                String mark = markDays1[i];
                //天正态总量
                int limitTotal = 0;
                int noLimitTotal = 0;
                markDaykeyAndArgs.add(CacheKey.AR_TASK2 + dayOfTime + mark);
                markDaykeyAndArgs.add(expireTime);
                Map<String, Integer> dayNumMap = markDayNumMap.get(mark);
                String[] dayStrs = dayNumMap.keySet().toArray(new String[0]);
                List<RetentRecordVo> markDayList = new ArrayList<>();
                for (String dayStr : dayStrs) {
                    RetentRecordVo recordVo = initMarkAllTotal(dayNumMap.get(dayStr), dayStr, mark, markTotalMap);
                    if (recordVo.getMark() != null) {
                        markDayList.add(recordVo);
                    }
                    int dayTotal = recordVo.getTaskNum();
                    markDaykeyAndArgs.add(dayStr + "total");
                    markDaykeyAndArgs.add(dayTotal + "");
                    limitTotal += dayTotal;
                    noLimitTotal += recordVo.getTotalTaskNum();
                }
                apkManager.putRetentTaskApkNum(mark, noLimitTotal);
                redisClient.put(CacheKey.AR_TASK_DAY3 + mark, markDayList);
                markNumMap.put(mark, limitTotal);
                markDaykeyAndArgs.add(1, limitTotal + "");
                if (markDaykeyAndArgs.size() > 3) {
                    String lua = "redis.call('HSET',KEYS[1], 'total', KEYS[2]) redis.call('expire',KEYS[1], KEYS[3]) for i=4,#(KEYS),2 do redis.call('HSET',KEYS[1], KEYS[i], KEYS[i+1]) end return true";
                    redisClient.eval(lua, ReturnType.BOOLEAN, markDaykeyAndArgs.toArray(new String[0]));
                }
            }
        }
        if (markBrushMap != null && !markBrushMap.isEmpty()) {
            String[] apks = markBrushMap.keySet().toArray(new String[0]);
            for (String apk : apks) {
                List<MarkDayTask> markDayTasks = markBrushMap.get(apk);
                List<String> markDaykeyAndArgs = new ArrayList<>();
                markDaykeyAndArgs.add(CacheKey.AR_BRUSH_TASK + dayOfTime + apk);
                for (MarkDayTask dayTask : markDayTasks) {
                    markDaykeyAndArgs.add(dayTask.getDay() + ":brushtotal");
                    markDaykeyAndArgs.add(dayTask.getTaskNum() + "");
                }
                String lua = "for i = 2,#(KEYS),2 do redis.call('HSET',KEYS[1], KEYS[i],KEYS[i+1]) end return true";
                redisClient.eval(lua, ReturnType.BOOLEAN, markDaykeyAndArgs.toArray(new String[0]));
            }
        }

    }


    /**
     * @param markTotal    天正态总量
     * @param retentDay
     * @param mark
     * @param markTotalMap 天留存总量
     * @return
     */
    private RetentRecordVo initMarkAllTotal(int markTotal, String retentDay, String mark, Map<String, Integer> markTotalMap) {

        RetentRecordVo recordVo = new RetentRecordVo();
        Integer dayTotal = markTotalMap.get(mark + retentDay);
        int limitTotal = 0;
        int noLimitTotal = 0;
        if (dayTotal == null) {
            limitTotal = 0;
        }
        //次数配置 按次数排序
        List<RetentTimesConfig> retentTimesConfigs = markRetentTimesMap.get(mark);
        String dayHashKey = CacheKey.AR_DAY_HASH_KEY + dayOfTime + mark;
        List<String> dayList = new ArrayList<>();
        dayList.add(CacheKey.AR_DAY_HASH_KEY + dayOfTime + mark);
        dayList.add(String.valueOf(DateUtil.leftTimeToMidNight() / 1000));

        String fieldKey = retentDay + ":" + 1 + ":exectotal";

        if (retentTimesConfigs == null || retentTimesConfigs.isEmpty()) {
            dayList.add(retentDay + ":" + 1 + ":total");
            dayList.add(String.valueOf(markTotal));
            recordVo.setTaskNum(markTotal);
            recordVo.setExecTimes(1);
            recordVo.setTaskType(1);
            recordVo.setDayOrMonth(Integer.parseInt(retentDay));
            recordVo.setApkId(markApkIdMap.get(mark));
            int dayExecTimesCount = 0;
            String dayExtimesExecTotal = redisClient.evalHget(dayHashKey, fieldKey);
            if (dayExtimesExecTotal != null) {
                dayExecTimesCount = Integer.parseInt(dayExtimesExecTotal);
            }
            noLimitTotal = dayTotal;
            int leftNum = markTotal - dayExecTimesCount;
            if (leftNum > 0) {
                recordVo.setMark(mark);
            }
            recordVo.setLeftNum(leftNum);

        } else {
            boolean flag = false;
            for (RetentTimesConfig timesConfig : retentTimesConfigs) {
                Integer execTimes = timesConfig.getExecTimes();
                Integer allRetentRate = timesConfig.getAllRetentRate();
                fieldKey = retentDay + ":" + execTimes + ":total";
                String execFieldKey = retentDay + ":" + execTimes + ":exectotal";
                int dayExtimesTotal = (int) (allRetentRate * markTotal / 100d);
                if (!flag) {
                    int dayExecTimesCount = 0;
                    String dayExtimesExecTotal = redisClient.evalHget(dayHashKey, execFieldKey);
                    if (dayExtimesExecTotal != null) {
                        dayExecTimesCount = Integer.parseInt(dayExtimesExecTotal);
                    }
                    int leftNum = dayExtimesTotal - dayExecTimesCount;
                    if (leftNum > 0) {
                        flag = true;
                        recordVo.setExecTimes(execTimes);
                        recordVo.setLeftNum(leftNum);
                        recordVo.setApkId(timesConfig.getApkId());
                        recordVo.setDayOrMonth(Integer.parseInt(retentDay));
                        recordVo.setTaskType(1);
                        recordVo.setMark(mark);
                        recordVo.setExecTimes(execTimes);
                        recordVo.setDayOrMonth(Integer.parseInt(retentDay));
                    }
                }
                noLimitTotal += (int) (dayTotal * timesConfig.getAllRetentRate() / 100d);
                limitTotal += dayExtimesTotal;
                dayList.add(fieldKey);
                dayList.add(String.valueOf(dayExtimesTotal));

            }
            recordVo.setTaskNum(limitTotal);
        }
        recordVo.setTotalTaskNum(noLimitTotal);
        String lua = "redis.call('expire',KEYS[1], KEYS[2]) for i=3,#(KEYS),2 do redis.call('HSET',KEYS[1], KEYS[i], KEYS[i+1]) end return true";
        redisClient.eval(lua, ReturnType.BOOLEAN, dayList.toArray(new String[0]));
        return recordVo;

    }


    /**
     * getTimeLimit:按照分钟下发任务量
     *
     * @param apk
     * @return
     * @throws ParseException
     * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
     */
    private double getTimeLimit(String apk) {
        try {
            Map<String, ChannelApkConfigTimeModel> map = channelLimitService.getTimeRetentModelByMark(apk);
            Calendar rightNow = Calendar.getInstance();
            rightNow.setTimeZone(TimeZone.getDefault());
            String current = DateFormatUtils.format(rightNow.getTime(), "yyyyMMdd HH");
            int mm = Integer.parseInt(DateFormatUtils.format(rightNow.getTime(), "mm"));
            int nowmm = mm;
            if (mm < 15) {
                current = current + ":00:00";
            } else {
                mm = (mm / 15) * 15;
                current = current + ":" + mm + ":00";
            }
            String first = current.substring(9);
            ChannelApkConfigTimeModel firsetModel = map.get(first);
            double firsetNum = firsetModel.getTimePercent();
            if (first.equals("00:00:00")) {
                return firsetNum * (nowmm % 15 / 15d);
            } else if (first.equals("23:45:00")) {
                return firsetNum;
            } else {
                rightNow.setTimeInMillis(new SimpleDateFormat("yyyyMMdd HH:mm:ss").parse(current).getTime() + 900000);
                // 下一个15分钟
                String nextDate = DateFormatUtils.format(rightNow.getTime(), "yyyyMMdd HH:mm:ss");
                String second = nextDate.substring(9);
                ChannelApkConfigTimeModel secondModel = map.get(second);
                double secodeNum = secondModel.getTimePercent();
                return firsetNum + ((secodeNum - firsetNum) * (nowmm % 15 / 15d));
            }
        } catch (Exception e) {
            LOGGER.info("mark [{}]的留存日限正太模型没有配置", apk);
        }
        return 0;
    }


    /**
     * queryMarkNumOfDay:数据库查询某天留存
     *
     * @return
     * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
     */
    private Map<String, Set<MarkDayTask>> queryMarkNumOfDay(Set<String> daySet) {
        Map<String, Set<MarkDayTask>> map = new HashMap<String, Set<MarkDayTask>>();
        for (String day : daySet) {
            Set<MarkDayTask> set = new HashSet<MarkDayTask>();
            List<ChannelRetentLogInfo> logList = channelRetentService.selectChannelRetentLogOfDay(day);
            if (logList != null && logList.size() > 0) {
                for (ChannelRetentLogInfo logInfo : logList) {
                    MarkDayTask markDayTask = new MarkDayTask();
                    markDayTask.setTaskNum(logInfo.getTotal());
                    markDayTask.setMark(logInfo.getMark());
                    markDayTask.setDay(day);
                    set.add(markDayTask);
                }
            }
            map.put(day, set);
        }
        return map;
    }


    /**
     * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
     */
    private void updateCanUseApks(Map<String, Integer> markNumMap) {
        if (markNumMap != null && !markNumMap.isEmpty()) {
            Set<String> apkSet = markNumMap.keySet();
            for (String apk : apkSet) {
                Integer leftNum = markNumMap.get(apk);
                if (leftNum == null || leftNum <= 0) {
                    apkSet.remove(apk);
                }
            }
            redisClient.put(CacheKey.VERR_APK_KEY3, apkSet, DateUtil.leftTimeToMidNight() / 1000);
        } else {
            redisClient.remove(CacheKey.VERR_APK_KEY3);
        }
    }


    /**
     * @return
     * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
     */
    private void initApkMap() {
        List<ChannelApkConfig> allApks = channelApkInfoService.selectAllSimpleApk();
        for (ChannelApkConfig config : allApks) {
            String mark = config.getMark();
            Integer apkId = config.getId();
            markApkIdMap.put(mark, apkId);
            apkIdMarkMap.put(apkId, mark);
        }
    }

    private void initRetentTimesMap() {

        List<RetentTimesConfig> configList = retentTimesDao.selectConfigList(null);
        markRetentTimesMap = new HashMap<>();
        if (configList != null && !configList.isEmpty()) {
            for (RetentTimesConfig config : configList) {
                List<RetentTimesConfig> tempList = markRetentTimesMap.get(config.getMark());
                if (tempList == null) {
                    tempList = new ArrayList<>();
                }
                tempList.add(config);
                markRetentTimesMap.put(config.getMark(), tempList);
            }
        }
    }

    /**
     * updateUnUseDayOfApk:更新不能使用的天任务
     *
     * @author <a href="mailto:xupengcheng@zhexinit.com" >徐鹏程</a>
     */
    private void updateUnUseApk(Map<String, Map<String, Integer>> markDayNumMap){

        int count = 0;
        for (String mark : markApkIdMap.keySet()) {
            count++;
            if (!markDayNumMap.containsKey(mark)) {
                if (count > 100) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count = 0;
                }
                apkManager.putRetentTaskApkNum(mark, 0);
                redisClient.evalHset(CacheKey.AR_TASK2 + dayOfTime + mark, "total", "0");
            }
        }
    }


}
